package com.winding.plugin.swagger;

import cn.hutool.core.util.ReUtil;
import com.google.common.collect.Maps;
import com.thoughtworks.qdox.model.JavaAnnotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.winding.plugin.swagger.entity.RequestMappingInfo;
import com.winding.plugin.swagger.entity.SwaggerDoc;
import com.winding.plugin.swagger.enums.ControllerObj;
import com.winding.plugin.swagger.enums.RequestMethod;
import com.winding.plugin.swagger.utils.JavaSourceUtils;
import com.winding.plugin.swagger.utils.SpringAnnotationUtils;
import com.winding.plugin.swagger.utils.TypeUtil;
import io.swagger.models.*;
import io.swagger.models.auth.ApiKeyAuthDefinition;
import io.swagger.models.auth.In;
import io.swagger.models.auth.SecuritySchemeDefinition;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.QueryParameter;
import io.swagger.models.properties.*;
import io.swagger.models.refs.GenericRef;
import io.swagger.models.utils.PropertyModelConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Stream;

/**
 * <p>TODO
 *
 * @author wind
 * @version 1.0
 * @since 2018/12/5
 */
@Slf4j
public class SwaggerDocReader extends AbstractSwaggerDocReader {
    public SwaggerDocReader(SwaggerDoc swaggerDoc, Swagger swagger, ClassLoader classLoader) {
        super(swagger);
        this.swaggerDoc = swaggerDoc;
        this.classLoader = classLoader;
    }

    private Map<String, JavaClass> classJavaClassMap;

    private SwaggerDoc swaggerDoc;

    private ClassLoader classLoader;

    @Override
    public Swagger read(Map<String, JavaClass> classJavaClassMap) {
        this.classJavaClassMap = classJavaClassMap;
        readControllerList();
        fixSwagger();
        return swagger;
    }

    private void fixSwagger() {
        swagger.setSwagger(swaggerDoc.getSwagger());
        swagger.setBasePath(swaggerDoc.getBasePath());
        swagger.setInfo(swaggerDoc.getInfo());
        swagger.setHost(swaggerDoc.getHost());
        List<Parameter> parameterList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(swaggerDoc.getCommonParams())) {
            for (String s : swaggerDoc.getCommonParams()) {
                if(StringUtils.isNotBlank(s)){
                    QueryParameter queryParameter = new QueryParameter();
                    queryParameter.setName(s);
                    parameterList.add(queryParameter);
                }
            }
        }
        List<Map<String, List<String>>> authMapList = new ArrayList<>();
        if (swaggerDoc.getAuthHeaders() != null) {
            Map<String, List<String>> map = Maps.newHashMap();
            Map<String, SecuritySchemeDefinition> apiKeyAuthDefinitionMap = Maps.newHashMap();
            for (String header : swaggerDoc.getAuthHeaders()) {
                ApiKeyAuthDefinition apiKeyAuthDefinition = new ApiKeyAuthDefinition(header, In.HEADER);
                apiKeyAuthDefinitionMap.put(header, apiKeyAuthDefinition);
                map.put(header, new ArrayList<>());
            }
            authMapList.add(map);
            this.swagger.setSecurityDefinitions(apiKeyAuthDefinitionMap);
            this.swagger.getPaths().forEach((k, v) -> {
                List<Operation> operations = v.getOperations();
                if (!CollectionUtils.isEmpty(operations)) {
                    for (int i = 0; i < operations.size(); i++) {
                        Operation operation = operations.get(i);
                        operation.setSecurity(authMapList);
                        for (Parameter parameter : parameterList) {
                            operation.addParameter(parameter);
                        }
                    }
                }
            });

        }
    }

    private void readControllerList() {
        Map<String, Path> paths = Maps.newHashMap();
        classJavaClassMap.forEach((k, v) -> {
            if (v.getPackage().getName().startsWith(swaggerDoc.getBasePackage())) {
                List<JavaAnnotation> annotationList = v.getAnnotations();
                for (JavaAnnotation ann : annotationList) {
                    if ((ann.getType().getName().equals(ControllerObj.Controller.name())
                            || ann.getType().getName().equals(ControllerObj.RestController.name()))
                    ) {
                        if (!SpringAnnotationUtils.isSkip(v)) {
                            log.info("init:{}", v.getFullyQualifiedName());
                            String a = TypeUtil.transString(ann.getProperty("value"));
                            v.getSuperClass().getFullyQualifiedName();
                            String[] doc = regexGroup(TypeUtil.API_METHOD_PATTERN, v.getComment(),a);
                            Class clazz;
                            try {
                                clazz = ClassUtils.getClass(classLoader, v.getFullyQualifiedName());
                            } catch (ClassNotFoundException e) {
                                e.printStackTrace();
                                log.warn("{}  ClassNotFoundException", v.getSimpleName());
                                return;
                            }
                            paths.putAll(processMethod(clazz, v));
                            Tag tag = new Tag();
                            tag.setName(StringUtils.isBlank(a) ? ann.getType().getSimpleName() : a);
                            tag.setDescription(doc[0]);
                            swagger.addTag(tag);
                        }
                        break;
                    }
                }

            }
        });
        swagger.paths(paths);
    }

    private Map<String, Path> processMethod(Class clazz, JavaClass javaClass) {
        String basePath = SpringAnnotationUtils.getControllerPath(javaClass);
        Map<String, Method> methodMap = JavaSourceUtils.getAllMethod(clazz);
        Map<String, Path> paths = Maps.newHashMap();
        List<JavaMethod> javaMethods = javaClass.getMethods();
        javaMethods.forEach(v -> {
            if (SpringAnnotationUtils.isSkip(v)) {
                return;
            }
            RequestMappingInfo requestMapping = SpringAnnotationUtils.getRequestMappingInfo(v);
            if (requestMapping == null) {
                return;
            }
            Method method = methodMap.get(  JavaSourceUtils.getUniqueName(v.getName(),v.getParameterTypes()));
            if (method == null || method.getParameterCount() != v.getParameters().size()) {
                return;
            }

            String[] doc = regexGroup(TypeUtil.API_METHOD_PATTERN, v.getComment(),requestMapping.getName());
            if (requestMapping.getValue() == null || requestMapping.getValue().length == 0) {
                requestMapping.setValue(new String[]{""});
            }
            Stream.of(requestMapping.getValue()).forEach(s -> {
                if(StringUtils.isBlank(s)){
                    return;
                }
                String url = TypeUtil.buildPath(basePath, s);
                Path path;
                if (paths.containsKey(url)) {
                    path = paths.get(url);
                } else {
                    path = new Path();
                }
                Operation operation = new Operation();
                operation.setConsumes(Arrays.asList(requestMapping.getConsumes()));
                operation.setProduces(Arrays.asList(requestMapping.getProduces()));
                operation.setParameters(processParameter(method, v));
                StringBuilder buffer = new StringBuilder();
                for (RequestMethod requestMethod : requestMapping.getMethod()) {
                    switch (requestMethod) {
                        case GET:
                            path.get(operation);
                            break;
                        case POST:
                            path.post(operation);
                            break;
                        case PUT:
                            path.put(operation);
                            break;
                        case DELETE:
                            path.delete(operation);
                            break;
                        case OPTIONS:
                            path.options(operation);
                            break;
                        default:
                    }
                    buffer.append(method.getName()).append("Using").append(requestMethod.name());
                }
                String operationId = buffer.toString();
                operation.setTags(Arrays.asList(javaClass.getSimpleName()));
                operation.addResponse("200", getResponse(method.getGenericReturnType()));
                operation.setSummary(doc[0]);
                operation.setOperationId(operationId);
                operation.setDescription(doc[1]);
                operation.setDeprecated(false);
                v.getAnnotations().stream().filter(t -> t.getType().getSimpleName().equals("Deprecated")).findAny()
                        .ifPresent(k -> operation.setDeprecated(true));
                paths.put(url, path);
            });
        });
        return paths;
    }

    private Response getResponse(Type genericReturnType) {
        Response response = new Response();
        if (genericReturnType.getTypeName().equals("void")) {
            return response;
        }
        String name;
        if (genericReturnType instanceof Class) {
            name = readModelMap(genericReturnType, classJavaClassMap.get(((Class) genericReturnType).getName()));
        } else if (genericReturnType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
            if (parameterizedType.getRawType().getTypeName().equals("org.springframework.http.ResponseEntity")) {
                genericReturnType = parameterizedType.getActualTypeArguments()[0];
            }
            if (!(genericReturnType instanceof Class)) {

                name = readType(genericReturnType);
            } else {
                name = readModelMap(genericReturnType, classJavaClassMap.get(((Class) genericReturnType).getName()));
            }
        } else {
            name = readType(genericReturnType);
        }
        if (StringUtils.isEmpty(name)) {
            response.setResponseSchema(new PropertyModelConverter().propertyToModel(modelConverters.readAsProperty(genericReturnType)));
        } else {

            if (genericReturnType instanceof ParameterizedType) {
                ParameterizedType parameterizedType1 = (ParameterizedType) genericReturnType;
                if (parameterizedType1.getRawType().equals(List.class)
                        || parameterizedType1.getRawType().equals(Set.class)) {
                    ArrayModel arrayModel = new ArrayModel();
                    response.setResponseSchema(arrayModel);
                    RefProperty refProperty = new RefProperty();
                    refProperty.set$ref("#/definitions/" + name);
                    arrayModel.setItems(refProperty);
                }
            } else {
                RefModel refModel = new RefModel();
                response.setResponseSchema(refModel);
                refModel.set$ref("#/definitions/" + name);
            }

        }
        return response;
    }

    /**
     * 解析parameter
     *
     * @param method
     * @param javaMethod
     * @return
     */
    private List<Parameter> processParameter(Method method, JavaMethod javaMethod) {
        List<Parameter> parameterList = new ArrayList<>();
        javaMethod.getParameters().forEach(v -> {
            if (ReUtil.isMatch(TypeUtil.API_PARAM_IGNORE_PATTERN, v.getComment())) {
                return;
            }
            java.lang.reflect.Parameter parameter = method.getParameters()[javaMethod.getParameters().indexOf(v)];
            String name;
            if (!(parameter.getParameterizedType() instanceof Class)) {
                name = readType(parameter.getParameterizedType());
            } else {
                name = readModelMap(parameter.getType(), classJavaClassMap.get(parameter.getType().getName()));
            }
            //判断该参数是否是body对象
            if (SpringAnnotationUtils.isRequestBody(v)) {
                parameterList.addAll(bodyParse
                        .parseParameter(parameter, v, javaMethod, method, name,
                                classJavaClassMap));
            } else {
                //如果是path param
                if (SpringAnnotationUtils.isPathParam(v)) {
                    parameterList.addAll(pathParse
                            .parseParameter(parameter, v, javaMethod, method, name,
                                    classJavaClassMap));
                } else {
                    parameterList.addAll(queryParse
                            .parseParameter(parameter, v, javaMethod, method, name,
                                    classJavaClassMap));
                }

            }
        });
        return parameterList;
    }

    private String readType(Type typeImpl) {
        JavaClass javaClass;
        String name = "";
        javaClass = classJavaClassMap.get(typeImpl.getTypeName());
        Map<String, Model> stringModelMap = modelConverters.read(typeImpl);
        for (Map.Entry<String, Model> stringModelEntry : stringModelMap.entrySet()) {
            treeProcess(stringModelEntry.getValue().getProperties(), stringModelEntry.getKey());
        }

        ParameterizedType parameterizedType = (ParameterizedType) typeImpl;
        Class rawType = (Class) parameterizedType.getRawType();
        Map<String, Type> stringClassMap = Maps.newHashMap();
        //如果是集合类型
        if (rawType.equals(List.class) || rawType.equals(Set.class)) {
            //并且是带泛型的list
            if (parameterizedType.getActualTypeArguments() != null
                    && parameterizedType.getActualTypeArguments().length > 0) {
                for (Type type : parameterizedType.getActualTypeArguments()) {
                    if (type instanceof ParameterizedType) {
                        name = readType(type);
                    } else {
                        name = readModelMap(type, classJavaClassMap.get(type.getTypeName()));
                    }
                }
            }
        } else if (rawType.equals(Map.class)) {
            log.debug("map");
        } else if (rawType instanceof Class) {
            javaClass = classJavaClassMap.get(rawType.getTypeName());
            TypeVariable<Class>[] typeVariables = rawType.getTypeParameters();
            Type[] types = parameterizedType.getActualTypeArguments();
            for (int i = 0; i < typeVariables.length; i++) {
                stringClassMap.put(typeVariables[i].getName(), types[i]);
            }
        }
        for (Map.Entry<String, Model> stringModelEntry : stringModelMap.entrySet()) {
            swagger.addDefinition(stringModelEntry.getKey(), stringModelEntry.getValue());
            if (javaClass != null) {
                JavaSourceUtils.readClassFieldDoc(stringModelEntry.getValue(), javaClass);
            }
            name = stringModelEntry.getKey();
            for (Map.Entry<String, Property> stringPropertyEntry : stringModelEntry.getValue().getProperties()
                    .entrySet()) {
                if (stringPropertyEntry.getValue() instanceof RefProperty) {
                    RefProperty property = (RefProperty) stringPropertyEntry.getValue();
                    //拿到该字段对应的类
                    try {
                        Field field = rawType.getDeclaredField(property.getName());
                        TypeVariable p = (TypeVariable) field.getGenericType();
                        String typeName = p.getTypeName();
                        Type type = stringClassMap.get(typeName);
                        if (type instanceof Class) {
                            readModelMap(type, classJavaClassMap.get(((Class) type).getName()));
                        } else {
                            readType(type);
                        }
                    } catch (NoSuchFieldException e) {
                        log.warn("", e);
                    }
                } else if (stringPropertyEntry.getValue() instanceof ArrayProperty) {
                    ArrayProperty property = (ArrayProperty) stringPropertyEntry.getValue();
                    try {
                        Field field = rawType.getDeclaredField(property.getName());
                        String typeName;
                        if (field.getGenericType() instanceof TypeVariable) {
                            TypeVariable p = (TypeVariable) field.getGenericType();
                            typeName = p.getTypeName();
                        } else {
                            typeName = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]
                                    .getTypeName();
                        }

                        Type type = stringClassMap.get(typeName);
                        if (type instanceof Class) {
                            readModelMap(type, classJavaClassMap.get(((Class) type).getName()));
                        } else {
                            readType(type);
                        }
                    } catch (Exception e) {
                        log.warn("", e);
                    }

                    log.debug("array");
                } else if (stringModelEntry.getValue() instanceof MapProperty) {
                    log.debug("map");
                }
            }
        }
        return name;
    }


    private void treeProcess(Map<String, Property> propertyMap, String k) {
        if (propertyMap == null) {
            log.warn("treeProcess propertyMap is null {}", k);
            return;
        }
        for (Map.Entry<String, Property> stringPropertyEntry : propertyMap.entrySet()) {
            if (stringPropertyEntry.getValue() instanceof ArrayProperty) {
                ArrayProperty arrayProperty = (ArrayProperty) stringPropertyEntry.getValue();
                if (arrayProperty.getItems() instanceof RefProperty) {
                    RefProperty refProperty = (RefProperty) arrayProperty.getItems();
                    try {
                        Field f = refProperty.getClass().getDeclaredField("genericRef");
                        f.setAccessible(true);
                        GenericRef genericRef = (GenericRef) f.get(refProperty);
                        if (StringUtils.equals(genericRef.getSimpleRef(), k)) {
                            arrayProperty.setItems(new ObjectProperty());
                        }
                    } catch (NoSuchFieldException | IllegalAccessException e) {
                        log.warn("treeProcess error");
                    }
                }
            }
        }
    }

    private String readModelMap(Type targetClass, JavaClass javaClass) {
        Map<String, Model> modelMap = modelConverters.read(targetClass);
        StringBuilder stringBuilder = new StringBuilder();
        if (!modelMap.isEmpty()) {
            modelMap.forEach((k, v) -> {
                stringBuilder.append(k);
                treeProcess(v.getProperties(), k);
                if (swagger.getDefinitions() != null && swagger.getDefinitions().get(k) != null) {
                    return;
                }
                swagger.addDefinition(k, v);
                if (javaClass != null) {
                    JavaSourceUtils.readClassFieldDoc(v, javaClass);
                }
                Map<String, Property> propertyMap = v.getProperties();
                propertyMap.forEach((k1, v1) -> {
                    if (v1 instanceof ArrayProperty) {
                        //如果list里面的item是对象
                        if (((ArrayProperty) v1).getItems() instanceof RefProperty) {
                            log.debug(v1.toString());
                            try {
                                Field field = ((Class) (targetClass)).getDeclaredField(v1.getName());
                                Type type = field.getGenericType();
                                if (type instanceof ParameterizedType) {
                                    ParameterizedType parameterizedType = (ParameterizedType) type;
                                    readModelMap(parameterizedType.getActualTypeArguments()[0], classJavaClassMap
                                            .get(parameterizedType.getActualTypeArguments()[0].getTypeName()));
                                }
                            } catch (NoSuchFieldException e) {
                                log.warn("", e);
                            }
                        }
                    } else if (v1 instanceof RefProperty) {
                        try {
                            Type realClass = ((Class) targetClass).getDeclaredField(v1.getName()).getGenericType();
                            readModelMap(realClass, classJavaClassMap.get(realClass.getTypeName()));
                        } catch (NoSuchFieldException e) {
                            log.warn("", e);
                        }
                        log.debug(v1.toString());
                    } else if (v1 instanceof MapProperty) {
                        Class target = (Class) targetClass;
                        try {
                            Type type = target.getDeclaredField(k1).getGenericType();
                            if (type instanceof ParameterizedType) {
                                ParameterizedType parameterizedType = (ParameterizedType) type;
                                for (Type type1 : parameterizedType.getActualTypeArguments()) {
                                    if (type1 instanceof Class) {
                                        readModelMap(type1, classJavaClassMap.get(((Class) type1).getName()));
                                    } else if (type1 instanceof ParameterizedType) {
                                        readType(type1);
                                    }
                                }
                            }
                        } catch (NoSuchFieldException e) {
                            log.warn("", e);
                        }
                    }
                });
            });
        }
        return stringBuilder.toString();
    }


    private String[] regexGroup(String pattern, String input,String defaultStr) {
        if(input==null){
            input="";
        }
        List<String> strs = ReUtil.findAll(pattern,input,1);
        if(strs.size()==0){
            strs.add(defaultStr);
            strs.add(defaultStr);
        }else if(strs.size()==1){
            strs.add(strs.get(0));
        }
        return strs.toArray(new String[strs.size()]);
    }
}
